Accelerometer 计步 算法
概述
目前的计步方案:
- 通过 GPS 获取运动距离, 反推步数.
- 通过 Accelerometer 传感器计算步数.
- Android4.4 以后, 使用 STEP COUNTER 和 STEP DETECTOR 两个传感器相结合.
- 以上各种方案优势互补, 都用.
第一种方案在室外可行, 但如果用户在室内运动, 则不可行.
第二种方案比较通用, 算法如果设计得好, 可以比较精确.
第三种方案耗电较低, 也比较精确, 可行.
这边暂时只对第二种方案进一步讨论.
Accerometer(三轴加速器)
模型
x, y, z 轴代表的方向如图所示:
算法核心
计算 x, y, z 的矢量和, 这样可以平衡在某一个方向数值过大造成的数据误差, 然后将该值与上一时间点的值进行比较, 判断是否为波峰或波谷.
如果检测到了波峰, 并且符合时间差以及阈值的条件, 则判定为 1 步;
如果符合时间差条件, 波峰波谷差值大于初始值, 则将该差值纳入阈值的计算中.
所以, 检测是否为 1 步, 就是检测符合条件的波峰. 条件有如下三个:
- 曲线连续上升的次数
- 波峰波谷的差值大于阈值
- 阈值是动态改变的
初始值
算法的开始, 要先设置一些初始值. 如:
- 动态阈值: initialValue = 1.3
- 初始阈值: threadValue = 2.0
- 波峰波谷时间差: timeInterval = 250
计算矢量和
\(gravity = (x^2 + y^2 + z^2)^{\frac{1}{2}}\)
检测
如果 gravity == 0, 说明是第一次检测, 将值赋给 gravityOld;
如果 gravity != 0, 就将当前值与 gravityOld 比较;
记录上一次的状态 lastStatus 是上升还是下降.
如果当前值比 gravityOld 大, 说明在上升, 更新是否上升的标志 isDirectionUp, 更新持续上升次数 continueUpCount.
如果当前值比 gravityOld 小, 说明在下降. 需要将持续上升次数保存到变量 continueUpFormerCount, 然后将 continueUpCount 置 0, isDirectionUp 置为 false.
判断是否为波峰, 主要是判断这几项: 不再上升(isDirectionUp), 上一次的状态是上升(lastStatus), 上一次上升的次数大于2(continueUpFormerCount), 上一次的矢量和大于20(gravityOld).
如果是波峰, 进一步判断时间差是否符合条件, 波峰波谷的差值是否符合条件. 如果符合条件, 则步数加 1.
如果时间差符合条件, 而波峰波谷的差值大于阈值, 则将该差值纳入阈值的计算中.
import React, { Component } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Accelerometer } from "react-native-sensors"; const Value = ({name, value}) => ( <View style={styles.valueContainer}> <Text style={styles.valueName}>{name}:</Text> <Text style={styles.valueValue}>{new String(value).substr(0, 8)}</Text> </View> ) class Step extends Component { constructor(props) { super(props); new Accelerometer({ updateInterval: 400 // defaults to 100ms }) .then(observable => { observable.subscribe(({x,y,z}) => this.setState({x,y,z})); }) .catch(error => { console.log("The sensor is not available"); }); this.state = {x: 0, y: 0, z: 0}; this.gravityOld = 0; // 上一次的矢量和 this.lastStatus = false; // 上一次的状态, 上升还是下降 this.isDirectionUp = false; // 是否继续上升 this.continueUpCount = 0; // 持续上升次数 this.continueUpFormerCount = 0; // 上一次持续上升的次数 this.peakOfWave = 0; // 波峰值 this.valleyOfWave = 0; // 波谷值 this.timeOfThisPeak = 0; // 到达波峰花费的时间 this.timeOfLastPeak = 0; // 上次到达波峰花费的时间 this.timeOfNow = 0; // 当前时间 this.timeInterval = 250; // 波峰波谷时间差 this.initialValue = 1.3; // 用于计算动态阈值 this.threadValue = 2.0; // 初始阈值 this.steps = 0; // 当前步数 this.tempValue = []; // 存放波峰波谷差值 this.tempCount = 0; } _getSteps = () => { this._detectNewStep(this._average()); return this.steps; } // 计算 x y z 的平均值 _average = () => Math.sqrt(this.state.x * this.state.x + this.state.y * this.state.y + this.state.z * this.state.z) _detectNewStep = ( gravity ) => { if (this.gravityOld === 0) { this.gravityOld = gravity; } else { if (this._detectPeak(gravity, this.gravityOld)) { this.timeOfLastPeak = this.timeOfThisPeak; this.timeOfNow = new Date().getTime(); console.log('threshold', this.peakOfWave-this.valleyOfWave); if ((this.timeOfNow - this.timeOfLastPeak >= this.timeInterval) && (this.peakOfWave - this.valleyOfWave >= this.threadValue)) { this.timeOfThisPeak = this.timeOfNow; this.steps = this.steps + 1; console.log('pinvon step', this.steps); } if ((this.timeOfNow - this.timeOfLastPeak >= this.timeInterval) && (this.peakOfWave - this.valleyOfWave >= this.initialValue)) { this.timeOfThisPeak = this.timeOfNow; this.threadValue = this._peakValleyThread(this.peakOfWave - this.valleyOfWave); console.log('pinvon', this.threadValue); } } } this.gravityOld = gravity; } _peakValleyThread = ( value ) => { console.log('pinvon', '_peakValleyThread', value); var tempThread = this.threadValue; if (this.tempCount < 4) { this.tempValue[this.tempCount] = value; this.tempCount = this.tempCount + 1; } else { this.tempThread = this._averageValue(this.tempValue, 4); for (var i = 1; i < 4; i++) { this.tempValue[i-1] = this.tempValue; } this.tempValue[3] = value; } return tempThread; } _averageValue = (value, n) => { var ave = 0; for (var index = 0; index < n; index++) { ave = ave + value[index]; } ave = ave / 4; if (ave > 0) { ave = 4.3; } else if (ave >= 7 && ave < 8) { ave = 3.3; } else if (ave >= 4 && ave < 7) { ave = 2.3; } else if (ave >= 3 && ave < 4) { ave = 2.0; } else { ave = 1.3; } return ave; } _detectPeak = (newValue, oldValue) => { this.lastStatus = this.isDirectionUp; if (newValue >= oldValue) { this.isDirectionUp = true; this.continueUpCount = this.continueUpCount + 1; } else { this.continueUpFormerCount = this.continueUpCount; this.continueUpCount = 0; this.isDirectionUp = false; } if (!this.isDirectionUp && this.lastStatus && (this.continueUpFormerCount >= 2 || oldValue >= 20)) { this.peakOfWave = oldValue; return true; } else if (!this.lastStatus && this.isDirectionUp) { this.valleyOfWave = oldValue; return false; } else { return false; } } render() { return ( <View style={styles.container}> <Text style={styles.headline}> Accelerometer values </Text> <Value name="x" value={this.state.x} /> <Value name="y" value={this.state.y} /> <Value name="z" value={this.state.z} /> <Value name="step" value={this._getSteps()} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, headline: { fontSize: 30, textAlign: 'center', margin: 10, }, valueContainer: { flexDirection: 'row', flexWrap: 'wrap', }, valueValue: { width: 200, fontSize: 20 }, valueName: { width: 50, fontSize: 20, fontWeight: 'bold' }, instructions: { textAlign: 'center', color: '#333333', marginBottom: 5, }, }); export default Step;
Generated by Emacs 25.x(Org mode 8.x)
Copyright © 2014 - Pinvon - Powered by EGO